/**
 *Copyright 2013 by dragon.
 *
 *File name: Downloader.java
 *Author:      dragon
 *Email:       fufulove2012@gmail.com
 *Blog:        http://blog.csdn.net/xidomlove
 *Version:     1.0.0
 *Date:        2013-10-4 下午2:51:34
 *Description: FTP,HTTP 共同操作,记录下载信息,写入文件等
 */
package com.dragon.jaxel.xtp;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.AccessController;
import java.security.PrivilegedAction;

import com.dragon.jaxel.Downloader;
import com.dragon.log.Logger;

/**
 * @author dragon8
 *
 */

/**
 * @author dragon8
 */
public abstract class XtpDownloader implements Downloader {

    /**
     * 总连接数
     */
    private int connectionCount;

    /**
     * 文件大小
     */
    private long size = -1;

    /**
     * 下载线程的数组
     */
    private DownloadConnection[] downloadConnections;

    /**
     * 保存的文件名
     */
    protected String savePathString;

    protected boolean isSupportRange = false;

    /**
     * @return the isSupportRange
     */
    @Override
    public boolean isSupportRange() {
        return isSupportRange;
    }

    /**
     * @return the savePathString
     */
    @Override
    public String getSavePathString() {
        return savePathString;
    }

    /**
     * 当前是否正在下载
     */
    private boolean bRunning = false;

    private int aliveConnectionCount;

    /**
     * 可随机写文件
     */
    private RandomAccessFile randomAccessFile;

    /**
     *
     */
    private FileChannel fileChannel;

    /**
     * 获取活跃的连接
     *
     * @return the aliveConnectionCount
     */
    public int getAliveConnectionCount() {
        return aliveConnectionCount;
    }

    /**
     * 所有下载线程是否已结束，有可能被中断，不一定已下载完成
     *
     * @return
     */
    @Override
    public boolean isDownLoading() {
        return aliveConnectionCount > 0;
    }

    /**
     * 是否文件已经下载完成
     *
     * @return
     */
    @Override
    public boolean isCompleted() {
        return getProgress() == getSize();
    }

    /**
     * 获取文件长度
     *
     * @return the size
     */
    @Override
    public long getSize() {
        if (size == -1) {
            size = getFileLength();
            if (size == -1) {
                Logger.getDefaultLogger().error("Get file size failed");
            }
        }
        return size;
    }

    /**
     * 获取总进度
     *
     * @return
     */
    @Override
    public long getProgress() {
        long progress = 0;
        for (int i = 0; i < connectionCount; i++) {
            progress += downloadConnections[i].bytesRead;
        }
        return progress;
    }

    /**
     *
     */
    protected XtpDownloader() {
        // TODO Auto-generated constructor stub
    }

    /**
     * 删除状态文件
     */
    @Override
    public void deleteSateFile() {
        File file = new File(savePathString + ".jaxel");
        if (file.exists()) {
            file.delete();
        }
    }

    /**
     * 保存下载信息到文件， 文件结构：<br/>
     * 1.string:class name. <br/>
     * 2.int: connection count.<br/>
     * 3.connetcions[];connections state.<br/>
     * 4.savePath <br/>
     * 5.subclass state.
     *
     * @throws IOException
     */
    @Override
    public void saveState() throws IOException {
        ObjectOutputStream outputStream = new ObjectOutputStream(
                new FileOutputStream(savePathString + ".jaxel"));
        outputStream.writeUTF(this.getClass().getName());
        outputStream.writeInt(connectionCount);
        for (int i = 0; i < connectionCount; i++) {
            outputStream.writeLong(downloadConnections[i].bytesBegin);
            outputStream.writeLong(downloadConnections[i].bytesEnd);
            outputStream.writeLong(downloadConnections[i].bytesRead);
        }
        outputStream.writeUTF(savePathString);
        saveDowanloadState(outputStream);
        outputStream.close();
    }

    /**
     * 读取上次未完成下载
     *
     * @param inputStream
     * @return
     * @throws FileNotFoundException
     * @throws IOException
     * @throws ClassNotFoundException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws IllegalArgumentException
     * @throws InvocationTargetException
     * @throws NoSuchMethodException
     * @throws SecurityException
     */
    @Override
    public void loadState(ObjectInputStream inputStream)
            throws FileNotFoundException, IOException, ClassNotFoundException,
            InstantiationException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException,
            NoSuchMethodException, SecurityException {
        connectionCount = inputStream.readInt();
        downloadConnections = new DownloadConnection[connectionCount];
        for (int i = 0; i < connectionCount; i++) {
            downloadConnections[i] = new DownloadConnection();
            downloadConnections[i].bytesBegin = inputStream.readLong();
            downloadConnections[i].bytesEnd = inputStream.readLong();
            downloadConnections[i].bytesRead = inputStream.readLong();
        }
        savePathString = inputStream.readUTF();
        loadDowanloadState(inputStream);
        inputStream.close();
    }

    /**
     * 子类保存需要保存到未完成下载的状态
     *
     * @param outputStream
     * @throws IOException
     */
    protected abstract void saveDowanloadState(ObjectOutputStream outputStream)
            throws IOException;

    /**
     * 读取上次未完成下载的状态，子类实现
     *
     * @param inputStream
     * @throws IOException
     * @throws ClassNotFoundException
     */
    protected abstract void loadDowanloadState(ObjectInputStream inputStream)
            throws IOException, ClassNotFoundException;

    /**
     * 开始下载
     *
     * @return 如果已经在下载或者上一次结束未完成（线程没有退出），则返回false
     * @throws IOException
     */
    @Override
    public boolean start() throws IOException {
        long len = getSize();
        int count;
        if (len < 5 * 1024 * 1024) {
            count = 2;
        } else if (len < 100 * 1024 * 1024) {
            count = 4;
        } else {
            count = 8;
        }
        return start(count);
    }

    /**
     * 停止下载
     */
    @Override
    public synchronized void stop() {
        Logger.getDefaultLogger().debug("aaaaa");
        bRunning = false;
        for (int i = 0; i < connectionCount; i++) {
            if (downloadConnections[i].bIsRunning) {
                try {
                    downloadConnections[i].bIsRunning = false;
                    Logger.getDefaultLogger().debug("ccccc");
                    closeConnection(downloadConnections[i].context);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    Logger.getDefaultLogger().debug(e.getMessage());
                }
            }
        }
        Logger.getDefaultLogger().debug("bbbb");
    }

    /**
     * 开始下载任务
     *
     * @param connectionCount
     * @return 如果已经在下载或者上一次结束未完成（线程没有退出）,或者获取文件长度失败，则返回false
     * @throws IOException
     */
    @Override
    public boolean start(int connectionCount) throws IOException {
        if (!isSupportRange) {
            // 不支持断点下载
            connectionCount = 1;
        }
        this.connectionCount = connectionCount;
        // 初始化连接
        if (getSize() == -1 || connectionCount <= 0) {
            return false;
        }
        long bytesPerConnetion = getSize() / connectionCount;
        downloadConnections = new DownloadConnection[connectionCount];
        long offset = 0;
        int i = 0;
        for (; i < (connectionCount - 1); i++) {
            downloadConnections[i] = new DownloadConnection(offset, offset
                    + bytesPerConnetion);
            offset += bytesPerConnetion;
        }

        downloadConnections[i] = new DownloadConnection(offset, getSize());

        // 调用resume 开始启动线程
        return resume();
    }

    /**
     * 继续下载
     *
     * @return 如果已经在下载或者上一次结束未完成（线程没有退出），则返回false
     * @throws IOException
     */
    @Override
    public boolean resume() throws IOException {
        if (getSize() == -1 || connectionCount <= 0) {
            return false;
        }
        if (isDownLoading()) {
            // 等待100毫秒
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(10);
                    if (!isDownLoading()) {
                        break;
                    }
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    Logger.getDefaultLogger().debug(e.getMessage());
                }
            }
        }
        if (isDownLoading()) {
            return false;
        }
        bRunning = true;
        aliveConnectionCount = connectionCount;
        File file = new File(savePathString);
        randomAccessFile = new RandomAccessFile(file, "rw");
        Logger.getDefaultLogger().info(
                String.format("File open: %s", file.getAbsolutePath()));
        fileChannel = randomAccessFile.getChannel();
        fileChannel.truncate(getSize());
        for (int i = 0; i < connectionCount; i++) {
            downloadConnections[i].connectionName = "" + i;
            downloadConnections[i].start();
        }
        return true;
    }

    /**
     * 暂停下载
     */
    @Override
    public void pause() {
        for (int i = 0; i < connectionCount; i++) {
            if (downloadConnections[i].inputStream != null) {
                synchronized (downloadConnections[i].inputStream) {
                    if (downloadConnections[i].inputStream != null) {
                        try {
                            downloadConnections[i].inputStream.close();
                        } catch (Exception e) {
                            // TODO Auto-generated catch block
                            Logger.getDefaultLogger().debug(e.getMessage());
                        }
                    }
                }
            }
        }
    }

    /**
     * 获取文件长度，需要子类实现
     *
     * @return
     */
    abstract long getFileLength();

    /**
     * 获取下载协议的字符串表示
     *
     * @return
     */
    public abstract String getProtocol();

    /**
     * 打开一个连接，需要子类实现
     *
     * @param context    供保存连接上下文信息
     * @param bytesBegin 该连接起始下载偏移
     * @param bytesEnd   下载结束位置
     * @return 输入流，用于读取并写入文件
     * @throws IOException
     */
    abstract InputStream openConnection(ConnectionContext context,
                                        final long bytesBegin, final long bytesEnd) throws IOException;

    protected void closeConnection(ConnectionContext context)
            throws IOException {
    }

    /**
     * 一个连接即将开始，可根据需要重写方法
     *
     * @param context
     */
    protected void onBegin(ConnectionContext context) {
    }

    /**
     * 强制释放内存映射
     *
     * @param mappedByteBuffer
     */
    static void unmapFileChannel(final MappedByteBuffer mappedByteBuffer) {
        try {
            if (mappedByteBuffer == null) {
                return;
            }
            mappedByteBuffer.force();
            AccessController.doPrivileged(new PrivilegedAction<Object>() {
                @Override
                public Object run() {
                    try {
                        Method getCleanerMethod = mappedByteBuffer.getClass()
                                .getMethod("cleaner", new Class[0]);
                        getCleanerMethod.setAccessible(true);
                        sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod
                                .invoke(mappedByteBuffer, new Object[0]);
                        cleaner.clean();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    return null;
                }
            });

        } catch (Exception e) {
            Logger.getDefaultLogger().debug(
                    String.format("Unmap file failed!%s", e.getMessage()));
        }
    }

    /**
     * 连接类，表示一个下载连接
     *
     * @author dragon8
     */
    class DownloadConnection implements Runnable {

        private ConnectionContext context = new ConnectionContext();

        /**
         * 下载开始偏移
         */
        long bytesBegin;

        /**
         * 下载结束的位置
         */
        long bytesEnd;

        /**
         * 总共已完成字节数
         */
        long bytesRead;

        final static int BUFFER_SIZE = 64 * 1024;

        /**
         * 最大内存映射文件20MB
         */
        final static int MAX_MEMORY_BUFFER_SIZE = 20 * 1024 * 1024;

        /**
         * 网络连接的流
         */
        InputStream inputStream = null;

        /**
         * 连接名字，方便日志查看
         */
        String connectionName = "unknown";

        /**
         * 线程是否正在运行
         */
        boolean bIsRunning;

        public DownloadConnection() {
            super();
            // TODO Auto-generated constructor stub
        }

        public DownloadConnection(long bytesBegin, long bytesEnd) {
            super();
            this.bytesBegin = bytesBegin;
            this.bytesEnd = bytesEnd;
            bytesRead = 0;
        }

        public void start() {
            bIsRunning = true;
            new Thread(this).start();
        }

        private MappedByteBuffer map(long begin, long size) throws IOException {
            int offset = (int) (begin % 4096);
            if (offset != 0) {
                //对齐4K字节.
                begin -= offset;
            }
            MappedByteBuffer byteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, begin, size + offset);
            Logger.getDefaultLogger()
                    .debug(String
                            .format("Connection:%s Map memory, offset:%d, size:%d",
                                    connectionName, begin,
                                    size));
            byteBuffer.position(offset);
            return byteBuffer;
        }

        /*
         * (non-Javadoc)
         *
         * @see java.lang.Thread#run()
         */
        /*
         * (non-Javadoc)
		 * 
		 * @see java.lang.Runnable#run()
		 */
        /*
         * (non-Javadoc)
		 * 
		 * @see java.lang.Runnable#run()
		 */
        @Override
        public void run() {
            // TODO Auto-generated method stub
            try {

                // 该文件块已经下载完成
                if ((bytesRead + bytesBegin) >= bytesEnd) {
                    throw new IOException("Connection has finished");
                }

                // 即将开始下载
                onBegin(context);

                // 打开连接
                inputStream = openConnection(context, bytesBegin + bytesRead,
                        bytesEnd);

                // 总共需要读取的数据大小
                long bytesToRead = bytesEnd - bytesBegin;

                // 本阶段需要读取的数据大小
                long stageToRead = bytesToRead - bytesRead;
                if (stageToRead > MAX_MEMORY_BUFFER_SIZE) {
                    stageToRead = MAX_MEMORY_BUFFER_SIZE;
                }
                // 本阶段已完成下载
                long stageRead = bytesRead;
                int len;
                byte[] buffer = new byte[BUFFER_SIZE];

                Logger.getDefaultLogger()
                        .info(String
                                .format("Connection:%s Start,download offset:%d, end:%d",
                                        connectionName, bytesBegin, bytesEnd));

                // 映射内存以快速写文件
                MappedByteBuffer mappedByteBuffer = map(bytesBegin + bytesRead,
                        stageToRead);
                try {
                    while (bRunning) {
                        // 网络读取
                        len = inputStream.read(buffer);
                        if (len == -1) {
                            Logger.getDefaultLogger()
                                    .info(String
                                            .format("Connection:%s Network disconnected,bytes has read:%d",
                                                    connectionName, bytesRead));
                            break;
                        }
                        if ((bytesRead + len) >= bytesToRead) {
                            // 本文件块已全部完成
                            len = (int) (bytesToRead - bytesRead);
                            mappedByteBuffer.put(buffer, 0, len);
                            bytesRead += len;
                            Logger.getDefaultLogger().info(
                                    String.format(
                                            "Connection:%s Download finished",
                                            connectionName));
                            break;
                        } else if ((bytesRead + len - stageRead) >= stageToRead) {
                            // 本阶段内存缓冲区写满，移动到下一区域块
                            int tmpLen = (int) (stageToRead - bytesRead + stageRead);
                            mappedByteBuffer.put(buffer, 0, tmpLen);
                            bytesRead += tmpLen;
                            stageRead = bytesRead;
                            unmapFileChannel(mappedByteBuffer);
                            stageToRead = bytesToRead - bytesRead;
                            if (stageToRead > MAX_MEMORY_BUFFER_SIZE) {
                                stageToRead = MAX_MEMORY_BUFFER_SIZE;
                            }
                            mappedByteBuffer = map(bytesBegin
                                    + bytesRead, stageToRead);
                            // 将剩下内容写入下一文件块
                            mappedByteBuffer.put(buffer, tmpLen, len - tmpLen);
                            continue;
                        }

                        // 写入文件
                        mappedByteBuffer.put(buffer, 0, len);
                        bytesRead += len;
                    }
                } catch (Exception e) {
                    // TODO: handle exception
                    Logger.getDefaultLogger().debug(
                            String.format("Connection:%s Exception caught: %s",
                                    connectionName, e.getMessage()));
                }

                // 释放资源
                unmapFileChannel(mappedByteBuffer);
            } catch (FileNotFoundException e) {
                // TODO Auto-generated catch block
                Logger.getDefaultLogger().debug(
                        String.format("Connection:%s Exception caught: %s",
                                connectionName, e.getMessage()));
            } catch (IOException e) {
                // TODO Auto-generated catch block
                Logger.getDefaultLogger().debug(
                        String.format("Connection:%s Exception caught: %s",
                                connectionName, e.getMessage()));
            } finally {
                // 更新下载状态
                synchronized (XtpDownloader.this) {
                    if (bIsRunning) {
                        bIsRunning = false;
                        try {
                            closeConnection(context);
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                    if (bRunning && (bytesRead + bytesBegin) < bytesEnd) {
                        // 本连接没有完成下载，重新连接
                        start();
                        Logger.getDefaultLogger()
                                .info(String
                                        .format("Connection:%s disconnected, trying to reconnect.",
                                                connectionName));
                    } else {
                        // 活跃连接数减1
                        aliveConnectionCount -= 1;
                        if (!isDownLoading()) {

                            // 所以连接已经完成
                            Logger.getDefaultLogger().info(
                                    "All Connection's thread exited");

                            // 关闭文件
                            try {
                                fileChannel.close();
                            } catch (IOException e) {
                                // TODO Auto-generated catch block
                                Logger.getDefaultLogger()
                                        .debug(String
                                                .format("Connection:%s Exception caught: %s",
                                                        connectionName,
                                                        e.getMessage()));
                            }
                            try {
                                randomAccessFile.close();
                                Logger.getDefaultLogger().info(
                                        String.format("File close: %s",
                                                new File(savePathString)
                                                        .getAbsolutePath()));
                            } catch (IOException e) {
                                // TODO Auto-generated catch block
                                Logger.getDefaultLogger()
                                        .debug(String
                                                .format("Connection:%s Exception caught: %s",
                                                        connectionName,
                                                        e.getMessage()));
                            }
                        }
                    }
                }
            }
            Logger.getDefaultLogger().debug(
                    String.format("Connection:%s Exit.", connectionName));
        }
    }
}
